OSSファンディングのために、会社で依存しているnpmライブラリを数えてみた
↑上記ブログのOSSサポートに際して、以下の目的でスクリプトを作成しました。
- 以下のnpmライブラリを集計する
- 社内のJS/TSプロジェクトが依存しているnpmライブラリ
- fundを受け付けているnpmライブラリ
実際の支援OSSの選定には、このスクリプトの結果以外にも様々の観点を考慮しましたが、定量的な情報があると選定理由を示しやすくなります。
リポジトリはこちら => https://github.com/yamatatsu/fundable-oss-summarizer
まえがき
実は今回このブログを書くにあたって、作成当時のコードを見直していたのですが、「ちょっとかっこ悪いな」と思うコードが散見されたのでリファクタリングしました。。。
要件
要件は端的に言うと以下の2点です
package-lock.json
をもとに、依存しているライブラリの支援用のURLを集計する- 依存元ライブラリが依存先ライブラリと同じ作者である場合は、カウントしない
それぞれ説明します。
package-lock.json
をもとに、依存しているライブラリの支援用のURLを集計する
npmのlockファイルであるpackage-lock.json
には、依存しているライブラリの支援用のURLが記載されています。
npm fund --json
というコマンドを実行することで、支援を受け付けているライブラリの情報がJSON形式で取得できます。
これはTSの型で表現すると以下のような形です。
type FundData = { funding: Funding | Funding[]; dependencies?: Record<string, FundData>; }; type Funding = { url: string };
FundData
が再帰している点がポイントです。
このjsonを読んで、以下のように支援先URLごとの依存数を集計したいと思います。
output:
type Result = Record<string, number>
依存元ライブラリが依存先ライブラリと同じ作者である場合は、カウントしない
ライブラリ作者が自分自身のライブラリを利用している場合は、支援先URLごとの依存数にカウントしないようにします。
例えば以下のようなnpm fund --json
の結果があると仮定します。
サンプルjson:
{ "ownerA_lib4_parent": { "funding": { "url": "https://example.com/ownerA1" }, "dependencies": { "ownerA_lib5_same_owner": { "funding": { "url": "https://example.com/ownerA1" } }, "ownerB_lib1_other_owner": { "funding": { "url": "https://example.com/ownerB1" }, "dependencies": { "ownerA_lib6_same_as_grandparent": { "funding": { "url": "https://example.com/ownerA1" } } } } } } }
このとき、それぞれのライブラリについては以下のように考えます。
ownerA_lib4_parent
: プロジェクトがトップレベルで依存しているライブラリ。カウントする。ownerA_lib5_same_owner
: 依存元ライブラリownerA_lib4_parent
と同じ作者であるため、カウントしない。ownerB_lib1_other_owner
: 依存元ライブラリownerA_lib4_parent
とは異なる作者であるため、カウントする。ownerA_lib6_same_as_grandparent
: 依存元を辿ると同一作者も現れるが、依存元ライブラリownerA_lib4_parent
とは異なる作者であるため、カウントする。
以上、これらの要件をもとにスクリプトを作成しました。
スクリプトの説明
言語はTS、ランタイムはDenoを選択しました。
main.ts
import { walk } from "https://deno.land/std@0.149.0/fs/walk.ts"; import { countByFundingUrl, FundData, Result } from "./lib.ts"; type FundJSON = { dependencies: Record<string, FundData>; }; let result: Result = {}; for await (const entry of walk("./jsons", { includeDirs: false })) { const json: FundJSON = JSON.parse(Deno.readTextFileSync(entry.path)); const _result = countByFundingUrl(json.dependencies); result = merge(result, _result); } console.info(Object.entries(result).sort((a, b) => b[1] - a[1]));
Denoのwalkが便利です。このためにDenoを選択したまである。
(そこまでメモリを気にする必要もないけど)1ファイルずつ展開するのでメモリに優しいです。
main.ts#merge()
function merge(a: Result, b: Result): Result { return Object.entries(b).reduce( (acc, [key, value]) => ({ ...acc, [key]: (acc[key] ?? 0) + value }), a, ); }
このコードは github copilot が勝手に書きました。
main.ts
の他の部分(merge()
を使っている箇所も含む)を書いたあとでfunction merge()
まで書けばあとは勝手に書いてくれます。いい時代になりましたね。
lib.ts#countByFundingUrl()
lib.ts
のなかに入っていきます。
export type FundData = { funding: Funding | Funding[]; dependencies?: Record<string, FundData>; }; type Funding = { url: string }; export type Result = Record<string, number>; export function countByFundingUrl( dependencies: Record<string, FundData>, ): Result { return count(flattenAndFilter(dependencies)); }
flattenAndFilter()
してcount()
してますね。へー。
lib.ts#flattenAndFilter()
これが大事なところですね。「親が同作者ならカウントしない」を達成するためにflatten
処理とfilter
処理を統合した関数にしました。
/** * flatten dependencies and filter it that is used by same maintainer * @param dependencies * @param parentUrl * @returns */ function flattenAndFilter( dependencies?: Record<string, FundData>, parentUrl?: string, ): string[] { if (!dependencies) return []; return Object.values(dependencies).flatMap( (fundData) => { const url = getFundingUrl(fundData); const flattened = flattenAndFilter(fundData.dependencies, url); return url === parentUrl ? flattened : [url, ...flattened]; }, ); }
これは流石に github copilot には書いてもらえませんでした。
でもflatten
しつつfilter
するのもArray.prototype.flatMap()
があるから簡単にかけます。いい時代になりましたね。
lib.ts#getFundingUrl()
これも github copilot には書いてもらえませんでした。たぶん。(当時のままなのでよく覚えてない。。)
「同じ作者なのに funding url が表記ゆれする。。。」という課題を対応するためのコードに当時の試行錯誤が伺えますね。えもい。
function getFundingUrl(fundData: FundData): string { const url = Array.isArray(fundData.funding) ? fundData.funding[0].url : fundData.funding.url; // https://github.com/sponsors/xxxxx => https://github.com/xxxxx if (url.startsWith("https://github.com/sponsors")) { return url.replace(/\/sponsors/, ""); } // https://github.com/xxxxx/yyyyy?sponsor=1 => https://github.com/chalk/xxxxx if (url.startsWith("https://github.com")) { return (url.match(/^https\:\/\/github\.com\/[^/]+/) ?? [""])[0]; } return url; }
lib.ts#count()
function count(arr: string[]): Result { return arr.reduce( (acc, str) => ({ ...acc, [str]: (acc[str] ?? 0) + 1 }), {} as Result, ); }
これもほとんど github copilot に書いてもらえました。いい時代になりましたね。
countBy
,groupBy
,chunk
,mapValues
など、よくある関数の名前を暗記してるとgithub copilotに手伝ってもらいやすいです。
おわりに
以上、fundを受け付けているnpmパッケージを集計するスクリプトを紹介しました。
ご参考までに。